/*
 * The contents of this web application are subject to the Mozilla Public License Version 
 * 1.1 (the "License"); you may not use this web application except in compliance with 
 * the License. You may obtain a copy of the License at http://www.mozilla.org/MPL/.
 * 
 * Software distributed under the License is distributed on an "AS IS" basis, 
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License 
 * for the specific language governing rights and limitations under the License.
 * 
 * The Original Code is owned by and the Initial Developer of the Original Code is 
 * Composite A/S (Danish business reg.no. 21744409). All Rights Reserved
 * 
 * Section 11 of the License is EXPRESSLY amended to include a provision stating 
 * that any dispute, including but not limited to disputes related to the enforcement 
 * of the License, to which Composite A/S as owner of the Original Code, as Initial 
 * Developer or in any other role, becomes a part to shall be governed by Danish law 
 * and be initiated before the Copenhagen City Court ("K�benhavns Byret")            
 */

var sass = require('../sass')
  , fs = require('fs')
  , url = require('url')
  , basename = require('path').basename
  , dirname = require('path').dirname
  , mkdirp = require('mkdirp')
  , join = require('path').join;

var imports = {};

/**
 * Return Connect middleware with the given `options`.
 *
 * Options:
 *
 *    `force`       Always re-compile
 *    `debug`       Output debugging information
 *    `src`         Source directory used to find .scss files
 *    `dest`        Destination directory used to output .css files
 *                  when undefined defaults to `src`.
 *    `outputStyle` Sass output style (nested,expanded, compact or compressed)
 *
 * Examples:
 *
 * Pass the middleware to Connect, grabbing .scss files from this directory
 * and saving .css files to _./public_.
 *
 * Following that we have a `staticProvider` layer setup to serve the .css
 * files generated by Sass.
 *
 *      var server = connect.createServer(
 *          sass.middleware({
 *              src: __dirname
 *            , dest: __dirname + '/public'
 *          })
 *        , connect.static(__dirname + '/public')
 *      );
 *
 * @param {Object} options
 * @return {Function}
 * @api public
 */

module.exports = function(options){
  options = options || {};

  // Accept src/dest dir
  if ('string' == typeof options) {
    options = { src: options };
  }

  // Force compilation
  var force = options.force;

  // Debug option
  var debug = options.debug;

  // Source dir required
  var src = options.src;
  if (!src) throw new Error('sass.middleware() requires "src" directory');

  // Default dest dir to source
  var dest = options.dest
    ? options.dest
    : src;

  var root = options.root || null;

  // Default compile callback
  options.compile = options.compile || function(){
    return sass
  };

  // Middleware
  return function sass(req, res, next){
    if ('GET' != req.method && 'HEAD' != req.method) return next();
    var path = url.parse(req.url).pathname;
    if (/\.css$/.test(path)) {
      var cssPath = join(dest, path)
        , sassPath = join(src, path.replace('.css', '.scss'))
        , sassDir = dirname(sassPath);

      if (root) {
        cssPath = join(root, dest, path.replace(dest, ''));
        sassPath = join(root, src, path
            .replace(dest, '')
            .replace('.css', '.scss'));
        sassDir = dirname(sassPath);
      }

      if (debug) {
        log('source', sassPath);
        log('dest', cssPath);
      }

      // Ignore ENOENT to fall through as 404
      function error(err) {
        next('ENOENT' == err.code
          ? null
          : err);
      }

      // Force
      if (force) return compile();

      // Compile to cssPath
      function compile() {
        if (debug) log('read', cssPath);
        fs.readFile(sassPath, 'utf8', function(err, str){
          if (err) return error(err);
          var style = options.compile();
          var paths = [];
          delete imports[sassPath];
          style.render(str, function(err, css){
            if (err) return next(err);
            if (debug) log('render', sassPath);
            imports[sassPath] = paths;
            mkdirp(dirname(cssPath), 0700, function(err){
              if (err) return error(err);
              fs.writeFile(cssPath, css, 'utf8', next);
            });
          }, {
            include_paths: [ sassDir ].concat(options.include_paths || options.includePaths || []),
            output_style: options.output_style || options.outputStyle
          });
        });
      }

      // Re-compile on server restart, disregarding
      // mtimes since we need to map imports
      if (!imports[sassPath]) return compile();

      // Compare mtimes
      fs.stat(sassPath, function(err, sassStats){
        if (err) return error(err);
        fs.stat(cssPath, function(err, cssStats){
          // CSS has not been compiled, compile it!
          if (err) {
            if ('ENOENT' == err.code) {
              if (debug) log('not found', cssPath);
              compile();
            } else {
              next(err);
            }
          } else {
            // Source has changed, compile it
            if (sassStats.mtime > cssStats.mtime) {
              if (debug) log('modified', cssPath);
              compile();
            // Already compiled, check imports
            } else {
              checkImports(sassPath, function(changed){
                if (debug && changed && changed.length) {
                  changed.forEach(function(path) {
                    log('modified import %s', path);
                  });
                }
                changed && changed.length ? compile() : next();
              });
            }
          }
        });
      });
    } else {
      next();
    }
  }
};

/**
 * Check `path`'s imports to see if they have been altered.
 *
 * @param {String} path
 * @param {Function} fn
 * @api private
 */

function checkImports(path, fn) {
  var nodes = imports[path];
  if (!nodes) return fn();
  if (!nodes.length) return fn();

  var pending = nodes.length
    , changed = [];

  nodes.forEach(function(imported){
    fs.stat(imported.path, function(err, stat){
      // error or newer mtime
      if (err || !imported.mtime || stat.mtime > imported.mtime) {
        changed.push(imported.path);
      }
      --pending || fn(changed);
    });
  });
}

/**
 * Log a message.
 *
 * @api private
 */

function log(key, val) {
  console.error('  \033[90m%s :\033[0m \033[36m%s\033[0m', key, val);
}
